-module(rtps_transport).

%%% @doc A transport is a communication channel used by RTPS. This can
%%% be either some network protocol or some other means of
%%% transport. A transport is defined by its locator which can for
%%% example be a IPv4 protocol, address and port number combination. A
%%% transport is used by one or more endpoints within a domain which
%%% use the transport for sending data and receiving data. Endpoints
%%% are readers, writers but also participants or the doamin
%%% itself. The transport process will in case of a network locator
%%% open a network socket and created a sending and receiving
%%% process. The sending process collects the data from the endpoints
%%% connected to this transport and sends it out while the receiving
%%% process receives the network data and takes care of distributing
%%% the data to the connected endpoints.
%%% The RTPS specifications define the UDP transport as the minimum
%%% standard transport mechanism.

-behaviour(gen_server).

%% -include_lib("kernel/include/logger.hrl").
-include("rtps.hrl").
-include("rtps_transport.hrl").
-include("rtps_psm.hrl").
-include("rtps_udp.hrl").

%% API
-export([start_link/2, open/4, send/2]).

%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
	 terminate/2, code_change/3]).

-export_type([endpoint/0]).
-type endpoint() :: #endpoint{}.

-record(data, {opts = [] :: [proplists:property()], socket :: term(), locator :: rtps:locator(),
	       receiver :: pid(), sender :: pid(), endpoints = [] :: [endpoint()]}).

%%%===================================================================
%%% API
%%%===================================================================

start_link(Locator, Opts) ->
    gen_server:start_link(?MODULE, [Locator, Opts], []).

-spec open(Pid, Endpoint_pid, Local_guid, Remote_guid) -> {ok, Sender} when
      Pid :: pid(),
      Endpoint_pid :: pid(),
      Local_guid :: rtps:guid(),
      Remote_guid :: rtps:guid() | unknown,
      Sender :: pid().
%% @doc Resister an endpoint with this transport. The transport
%% supervisor has already started the transport before this function
%% is called. The endpoint is added to the associated endpoints of
%% this transport. The function returns the pid of the `rtps_sender'
%% process which handles the collecting and processing of the
%% submessages.
open(Pid, Endpoint_pid, Local_guid, Remote_guid) ->
    gen_server:call(Pid, {open, Endpoint_pid, Local_guid, Remote_guid}).

-spec send(Pid, Msg) -> ok when
      Pid :: pid(),
      Msg :: rtps_psm:message_r().
%% Send data through the communication channel (socket) controlled by
%% this transport. Endpoints never use this function directly but will
%% call the `rtps_sender' process which is part of the transport. The
%% `rtps_sender' process uses this function to send the collected
%% data.
send(Pid, Msg) ->
    gen_server:call(Pid, {send, Msg}).

%%%===================================================================
%%% gen_server callbacks
%%%===================================================================

%% @private

%% On initialisation of the transport, a transport channel is created
%% based on the type of locator. In case of a network locator, such as
%% a IPv4 locator, a network socket is created. Also a sending and
%% receiving process are spawned which will collect and distribute
%% data from and to the endpoints the transport is connected to.
init([#locator_udpv4{address = Address, port = Port}, Opts]) ->
    init([#locator{kind = udpv4, address = Address, port = Port}, Opts]);
init([#locator{kind = udpv4, address = Address, port = Port} = Locator, Opts]) ->
    Multicast_loop = proplists:get_value(multicast_loop, Opts, false),
    MTU = proplists:get_value(mtu, Opts, ?MTU),
    Buffer_options = [],
    Options = [binary, {active, true}, {reuseaddr, true}, {ip, Address} | Buffer_options],
    Multicast = is_multicast(Address),

    Options1 = case Multicast of
		   true ->
		       [{multicast_loop, Multicast_loop},
			{add_membership, {Address, {0,0,0,0}}} | Options];
		   false ->
		       Options
	       end,
    {ok, Socket} = gen_udp:open(Port, Options1),
    {ok, Sender} = rtps_sender:start_link(self(), Locator, Multicast, MTU),
    rtps_sender:can_send(Sender),
    {ok, Receiver} = rtps_receiver:start_link(),
    process_flag(trap_exit, true),
    %% TODO: Only store relevant options. Now all opts are stored that
    %% are passed on while starting the transport.
    {ok, #data{opts = Opts, socket = Socket, locator = Locator, receiver = Receiver,
	       sender = Sender, endpoints = []}}.

%% @private

%% An endpoint which wants to connect to a transport must call this
%% function to get registerd with the transport. The transport will
%% add the endpoint to the known endpoints which enables the receiving
%% process to return received data to the appropriate process. On
%% registering with the transport, the registering process will be
%% returned the Pid of the sending process which now can be used to
%% send data to the transport.
handle_call({open, Endpoint_pid, Local_guid, Remote_guid}, _From,
	    #data{sender = Sender, endpoints = Endpoints} = Data) ->
    %% Link to the reader_locator, reader_proxy, writer_proxy or
    %% whatever source of messages within the system. If one of these
    %% sources exits because of a normal stop or some error, we have
    %% to remove them from the list of endpoints and if that list is
    %% empty, terminate alltogether. Also, if the transport goes
    %% down due to some error, we want the endpoints to go down as
    %% well. So, we use a link instead of a monitor.
    link(Endpoint_pid),
    %% NB: The Pid is the process pid of either a rtps writer or rtps
    %% reader, not the reader_locator / proxy etc.
    Endpoint = #endpoint{local_guid = Local_guid, remote_guid = Remote_guid, pid = Endpoint_pid},
    Reply = {ok, Sender},
    {reply, Reply, Data#data{endpoints = [Endpoint | Endpoints]}};
%% The process which collects data from the connected endpoints, calls
%% the transport for the actual sending of the data. On completion of
%% sending the data, the sending process is acknowledged for doing
%% so. (The sending process is implemented as a state machine which
%% needs explicit confirmation of the data being send.)
handle_call({send, Msg}, _From, 
	    #data{socket = Socket,
		  locator = #locator{kind = udpv4, address = Address, port = Port},
		  sender = Pid} = Data) ->
    gen_udp:send(Socket, Address, Port, Msg),    
    rtps_sender:can_send(Pid),
    {reply, ok, Data};
%% TODO: remove sinkhole
handle_call(_Request, _From, Data) ->
    Reply = ok,
    {reply, Reply, Data}.

%% @private
%% TODO: remove sinkhole
handle_cast(_Msg, Data) ->
    {noreply, Data}.

%% @private

%% On incomming data, the incomming data is decoded and checked for
%% minimum correctness. The decoed message is send to the receiving
%% process which takes apart the message and distributes it to the
%% endpoints connected to the transport.
handle_info({udp, _Socket, Address, Port, <<Bin:20/bytes, _/binary>> = Packet},
	    #data{receiver = Receiver, endpoints = Endpoints} = Data) ->
    case rtps_psm:dec_header(Bin) of
	%% TODO: do something with the message's version.
	#header{protocol_version = _Version} -> 
	    Message = rtps_psm:dec_message(Packet),
	    rtps_receiver:recv(Receiver, udp, Address, Port, Message, Endpoints);
	_ -> % Not a RTPS message, ignore
	    ok
    end,
    {noreply, Data};
%% In case an endpoint connected to this transport terminates, for
%% whatever reason, the connected endpoint is to be removed from the
%% list of endpoints. If there are no remaining endpoints left, the
%% transport itself can be terminated.
handle_info({'EXIT', Pid, _Info}, #data{endpoints = Endpoints} = Data) ->
    case lists:keytake(Pid, #endpoint.pid, Endpoints) of
	{value, _Eendpoint, []} ->
	    {stop, normal, Data#data{endpoints = []}};
	{value, _Endpoint, Endpoints1} ->
	    {noreply, Data#data{endpoints = Endpoints1}};
	_ ->
	    {noreply, Data}
    end;
%% TODO: remove sinkhole
handle_info(_Info, Data) ->
    {noreply, Data}.

%% @private
terminate(_Reason, #data{endpoints = Endpoints} = _Data) ->
    [unlink(Pid) || #endpoint{pid = Pid} <- Endpoints],
    ok.

%% @private
code_change(_OldVsn, Data, _Extra) ->
    {ok, Data}.

%%%===================================================================
%%% Internal functions
%%%===================================================================

%% @private 

%% By examining the IPv4 or IPv6 address, determine if this is a multi
%% or unicast address.
is_multicast({MSB, _, _, _}) ->
    case <<MSB:8>> of
	<<1:1, 1:1, 1:1, 0:1, _:4>> ->
	    true;
	_ ->
	    false
    end;
is_multicast({MSW, _, _, _, _, _, _, _}) ->
    case <<MSW:16>> of
	<<16#ff:8, _:8>> ->
	    true;
	_ ->
	    false
    end.
